// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using ILLink.RoslynAnalyzer.DataFlow;
using ILLink.Shared.DataFlow;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.FlowAnalysis;
using LocalStateValue = ILLink.RoslynAnalyzer.DataFlow.LocalStateAndContext<
    ILLink.Shared.DataFlow.ValueSet<ILLink.Shared.DataFlow.SingleValue>,
    ILLink.RoslynAnalyzer.DataFlow.FeatureContext
>;
using MultiValue = ILLink.Shared.DataFlow.ValueSet<ILLink.Shared.DataFlow.SingleValue>;

namespace ILLink.RoslynAnalyzer.TrimAnalysis
{
    internal sealed class TrimDataFlowAnalysis : LocalDataFlowAnalysis<
        MultiValue,
        FeatureContext,
        ValueSetLattice<SingleValue>,
        FeatureContextLattice,
        TrimAnalysisVisitor,
        FeatureChecksValue>
    {
        public TrimAnalysisPatternStore TrimAnalysisPatterns { get; }

        private DataFlowAnalyzerContext _dataFlowAnalyzerContext;

        // The initial state of the feature context is None, meaning that
        // no features are enabled at the beginning of the entry block.
        // This way, calls to all Requires-annotated APIs will warn unless
        // guarded by a feature check.
        public TrimDataFlowAnalysis(
            OperationBlockAnalysisContext context,
            DataFlowAnalyzerContext dataFlowAnalyzerContext,
            IOperation operationBlock)
            : base(
                context,
                operationBlock,
                default(ValueSetLattice<SingleValue>),
                new FeatureContextLattice(),
                initialContext: FeatureContext.None)
        {
            TrimAnalysisPatterns = new TrimAnalysisPatternStore(lattice.LocalStateLattice.Lattice.ValueLattice, lattice.ContextLattice);
            _dataFlowAnalyzerContext = dataFlowAnalyzerContext;
        }

        public void ReportDiagnostics(Action<Diagnostic> reportDiagnostic)
        {
            TrimAnalysisPatterns.ReportDiagnostics(_dataFlowAnalyzerContext, reportDiagnostic);
        }

        protected override TrimAnalysisVisitor GetVisitor(
            ISymbol owningSymbol,
            ControlFlowGraph methodCFG,
            ImmutableDictionary<CaptureId, FlowCaptureKind> lValueFlowCaptures,
            InterproceduralState<MultiValue, ValueSetLattice<SingleValue>> interproceduralState)
         => new(Context.Compilation, lattice, owningSymbol, methodCFG, lValueFlowCaptures, TrimAnalysisPatterns, interproceduralState, _dataFlowAnalyzerContext);

#if DEBUG
#pragma warning disable CA1805 // Do not initialize unnecessarily
        // Set this to a method name to trace the analysis of the method.
        private readonly string? traceMethod = null;

        private bool trace = false;

        // Set this to true to print out the dataflow states encountered during the analysis.
        private readonly bool showStates = false;

        private static readonly TracingType tracingMechanism = Debugger.IsAttached ? TracingType.Debug : TracingType.Console;
#pragma warning restore CA1805 // Do not initialize unnecessarily
        private ControlFlowGraphProxy cfg;

        private enum TracingType
        {
            Console,
            Debug
        }

        public override void TraceStart(ControlFlowGraphProxy cfg)
        {
            this.cfg = cfg;
            var blocks = cfg.Blocks.ToList();
            string? methodName = null;
            foreach (var block in blocks)
            {
                if (block.Block.Operations.FirstOrDefault() is not IOperation op)
                    continue;

                var method = op.Syntax.FirstAncestorOrSelf<MethodDeclarationSyntax>();
                if (method is MethodDeclarationSyntax)
                    methodName = method.Identifier.ValueText;

                break;
            }

            if (methodName?.Equals(traceMethod) == true)
                trace = true;
            if (trace)
                TraceWriteLine("Tracing method " + methodName);
        }

        public override void TraceVisitBlock(BlockProxy block)
        {
            if (!trace)
                return;

            TraceWrite("block " + block.Block.Ordinal + ": ");
            if (block.Block.Operations.FirstOrDefault() is IOperation firstBlockOp)
            {
                TraceWriteLine(firstBlockOp.Syntax.ToString());
            }
            else if (block.Block.BranchValue is IOperation branchOp)
            {
                TraceWriteLine(branchOp.Syntax.ToString());
            }
            else
            {
                TraceWriteLine("");
            }
            TraceWrite("predecessors: ");
            foreach (var predecessor in cfg.GetPredecessors(block))
            {
                var predProxy = predecessor.Source;
                TraceWrite(predProxy.Block.Ordinal + " ");
            }
            TraceWriteLine("");
        }

        private static void TraceWriteLine(string tracingInfo)
        {
            switch (tracingMechanism)
            {
                case TracingType.Console:
                    // Analyzers should not be writing to the console,
                    // but this is only used for debugging purposes and is off by default.
#pragma warning disable RS1035
                    Console.WriteLine(tracingInfo);
#pragma warning restore RS1035
                    break;
                case TracingType.Debug:
                    Debug.WriteLine(tracingInfo);
                    break;
                default:
                    throw new NotImplementedException(message: "invalid TracingType is being used");
            }
        }

        private static void TraceWrite(string tracingInfo)
        {
            switch (tracingMechanism)
            {
                case TracingType.Console:
                    // Analyzers should not be writing to the console,
                    // but this is only used for debugging purposes and is off by default.
#pragma warning disable RS1035
                    Console.Write(tracingInfo);
#pragma warning restore RS1035
                    break;
                case TracingType.Debug:
                    Debug.Write(tracingInfo);
                    break;
                default:
                    throw new NotImplementedException(message: "invalid TracingType is being used");
            }
        }

        private static void WriteIndented(string? s, int level)
        {
            if (s is not null)
            {
                var reader = new StringReader(s);
                string? line;
                while ((line = reader.ReadLine()) != null)
                {
                    if (line.Length != 0)
                    {
                        TraceWrite(new string('\t', level));
                        TraceWriteLine(line);
                    }
                }
            }
        }

        public override void TraceEdgeInput(
            IControlFlowGraph<BlockProxy, RegionProxy>.ControlFlowBranch branch,
            LocalStateValue state
        )
        {
            if (trace && showStates)
            {
                var source = branch.Source.Block.Ordinal;
                var target = branch.Destination?.Block.Ordinal;
                WriteIndented($"--- Edge from [{source}] to [{target}] ---", 1);
                WriteIndented(state.ToString(), 2);
            }
        }

        public override void TraceBlockInput(
            LocalStateValue normalState,
            LocalStateValue? exceptionState,
            LocalStateValue? exceptionFinallyState
        )
        {
            if (trace && showStates)
            {
                WriteIndented("--- before transfer ---", 1);
                WriteIndented("normal state:", 1);
                WriteIndented(normalState.ToString(), 2);
                WriteIndented("exception state:", 1);
                WriteIndented(exceptionState?.ToString(), 2);
                WriteIndented("finally exception state:", 1);
                WriteIndented(exceptionFinallyState?.ToString(), 2);
            }
        }

        public override void TraceBlockOutput(
            LocalStateValue normalState,
            LocalStateValue? exceptionState,
            LocalStateValue? exceptionFinallyState
        )
        {
            if (trace && showStates)
            {
                WriteIndented("--- after transfer ---", 1);
                WriteIndented("normal state:", 1);
                WriteIndented(normalState.ToString(), 2);
                WriteIndented("exception state:", 1);
                WriteIndented(exceptionState?.ToString(), 2);
                WriteIndented("finally state:", 1);
                WriteIndented(exceptionFinallyState?.ToString(), 2);
            }
        }
#endif
    }
}
